import { CSSProperties, dom, Nodes } from '@knno/dom';
import { empty } from '../empty/empty';
import { gridCellPaddingVar, gridLineHeightVar, gridStyle } from './grid.css';
import { GridBase, genWidth, MeasureType, RowType, GridBaseData } from './gridBase';
import { GRID_CELL_PADDING, GRID_LINE_HEIGHT, GRID_CELL_BORDER_BOTTOM } from './gridBaseConfig';
import { size2px } from '../../tools/utils';
import { DATA } from '../symbol';
import { assignInlineVars } from '@vanilla-extract/dynamic';

const CACHE_TIME = 200;

export type GridConfig = {
	borderBottom: number;
	lineHeight: string;
	cellPadding: string;
};

export type GridData = GridBaseData & {
	head: Nodes;
	anchor: Nodes;
	body: Nodes;
	css: CSSProperties[];
	lh: number[]; // extHeight
	lt: number[]; // lineTop
	first: number | null; // firstLine
	lineHeightCache: { value: number; timeout: NodeJS.Timeout } | null;
	config: GridConfig;
};

export interface Grid {
	[DATA]: GridData;
}

export class Grid extends GridBase {
	constructor() {
		const head = dom.div('head');
		const body = dom.div('body');
		const anchor = dom.div('anchor');
		super(dom.div(gridStyle).append(head, body, anchor));
		this[DATA].css = [];
		this[DATA].lh = [];
		this[DATA].lt = [];
		this[DATA].first = null;
		this[DATA].lineHeightCache = null;
		this[DATA].config = {
			borderBottom: GRID_CELL_BORDER_BOTTOM,
			lineHeight: GRID_LINE_HEIGHT,
			cellPadding: GRID_CELL_PADDING,
		};
		this[DATA].head = head;
		// this[DATA].head = head;
		this[DATA].anchor = anchor;
		// this[DATA].anchor = anchor;
		this[DATA].body = body;
		// this[DATA].body = body;
		this.headLine(true).showEmpty(true).columnSize('auto');
		let scrollTop = this.scrollTop();
		this.on('scroll', () => {
			const newTop = this.scrollTop();
			if (scrollTop != newTop) {
				scrollTop = newTop;
				this.renderBody();
			}
		});
		const resizeObserver = new ResizeObserver(() => {
			this.refresh();
		});
		resizeObserver.observe(this.elems()[0]);

		const insObserver = new IntersectionObserver(
			(entries) => {
				if (entries[0].isIntersecting) {
					this.emit(new Event('scrollbottom', { bubbles: false }));
				}
			},
			{ root: this.elems()[0] }
		);
		insObserver.observe(anchor.elems()[0]);
	}

	config(options: Partial<GridConfig>): this {
		if (options.lineHeight != undefined) {
			this[DATA].config.lineHeight = options.lineHeight;
			this.css(assignInlineVars({ [gridLineHeightVar]: options.lineHeight }));
		}
		if (options.cellPadding != undefined) {
			this[DATA].config.cellPadding = options.cellPadding;
			this.css(assignInlineVars({ [gridCellPaddingVar]: options.cellPadding }));
		}
		if (options.borderBottom != undefined) {
			this[DATA].config.borderBottom = options.borderBottom;
		}
		this.refresh();
		return this;
	}

	protected override setActiveRow(rowIdx: number | null, focus: boolean): void {
		this.query(':scope>div.body>div.line').removeClass('active');
		this[DATA].actived = rowIdx;
		if (rowIdx !== null) {
			const tr = this.query(`:scope>div.body>div.line.r${rowIdx}`);
			tr.addClass('active');
			if (focus) {
				const offset = this[DATA].body.top();
				const headerHeight = this.headLine() ? this[DATA].head.height() : 0;

				if (tr.length > 0) {
					if (tr.top() + offset + tr.height() > this.scrollTop() + this.innerHeight()) {
						// after the viewport.
						this.scrollTop(tr.top() + offset + tr.height() - this.innerHeight());
					}
					if (tr.top() + offset < this.scrollTop() + headerHeight) {
						// before the viewport.
						this.scrollTop(tr.top() + offset - headerHeight);
					}
				} else {
					const lineHeight = this.getLineHeight();
					const top = this.getLineOffset(headerHeight, lineHeight, rowIdx);
					if (top + lineHeight > this.scrollTop() + this.innerHeight()) {
						// after the viewport.
						this.scrollTop(top + lineHeight - this.innerHeight());
					}
					if (top < this.scrollTop() + headerHeight) {
						// before the viewport.
						this.scrollTop(top - headerHeight);
					}
				}
			}
		}
	}

	protected override renderSelect() {
		const columns = this.getColumns();
		const th = this.query(`:scope>div.head>div.c0`);
		if (th.length > 0) {
			th.html('').append(columns[0].formatter?.({}, null, -1, 0) ?? []);
		}
		const tds = this.query(`:scope>div.body>div.line>div.c0`);
		tds.forEach((td) => {
			const cell = dom.wrap(td);
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
			cell.html('').append(columns[0].formatter?.([], null, (td.parentElement as any).lineNo as number, 0) ?? []);
		});
	}

	protected override genStyle() {
		if (!this[0].isConnected) return false;
		const columns = this.adjustColumnSize();
		const autoColumnSize = this.columnSize() === 'auto';
		this[DATA].css = [];
		columns.forEach((col, idx) => {
			const width = genWidth(col.width);
			const cells = this.query(`div.head>div.c${idx}`); // ,div.line>div.c${idx}
			const css: CSSProperties = { width, position: undefined, left: '', zIndex: '' };
			if (col.fixed) {
				css.flexShrink = '0';
				css.flexGrow = '0';
			} else if (autoColumnSize) {
				css.flexShrink = '0';
				css.flexGrow = size2px(width, this[0] as HTMLElement, false) + '';
			}
			cells.css(css);
			if (idx < this.fixColumns()) {
				css.position = 'sticky';
				css.left = (this[DATA].head.child(idx)[0] as HTMLElement).offsetLeft + 'px';
				css.zIndex = '1';
				cells.css(css);
			}
			this[DATA].css.push(css);
		});
		return true;
	}

	protected measure(reason: MeasureType): void {
		if (reason !== 'col-fixed' && reason !== 'selection') {
			if (reason === 'current-page') {
				this[DATA].first = null;
			} else {
				// this[FILLER] = 0;
				this[DATA].lh = [];
				this[DATA].lt = [];
				this[DATA].first = null;
				this[DATA].body.css('top', '0px');
			}
			//this[DATA].anchor.css('top', '0px');
		}
	}

	protected override renderHead() {
		if (!this[0].isConnected) return false;
		const columns = this.getColumns();
		this[DATA].head.html('');
		const headLine = this.headLine();
		this[DATA].head.append(
			...columns.map((col, colIdx) => (headLine ? this.renderHeadCell(dom.div(), col, colIdx) : []))
		);
		// add a ruler line
		this.genStyle();
	}

	private clearLineHeightCache = () => (this[DATA].lineHeightCache = null);

	getLineHeight() {
		if (this[DATA].lineHeightCache != null) {
			return this[DATA].lineHeightCache.value;
		}
		let lineHeight = size2px(this[DATA].config.lineHeight, this[0] as HTMLElement, true) ?? 21;
		const cellPadding = size2px(this[DATA].config.cellPadding, this[0] as HTMLElement, true) ?? 7;
		lineHeight += cellPadding * 2 + this[DATA].config.borderBottom;
		this[DATA].lineHeightCache = {
			value: lineHeight,
			timeout: setTimeout(this.clearLineHeightCache, CACHE_TIME),
		};
		return lineHeight;
	}

	private getLineOffset(headHeight: number, lineHeight: number, line: number): number {
		if (this[DATA].lt[line] !== undefined) {
			// console.log('cached');
			return this[DATA].lt[line];
		} else {
			let lastLine = -1;
			for (let i = line - 1; i >= 0; i--) {
				if (this[DATA].lt[i] !== undefined) {
					lastLine = i;
					break;
				}
			}
			if (lastLine >= 0) {
				// console.log('compute from previouse line: ', lastLine, lastLine - line);
				const offset =
					this[DATA].lt[lastLine] +
					(line - lastLine) * lineHeight +
					this[DATA].lh.slice(lastLine, line).reduce((p, v) => p + v, 0);
				this[DATA].lt[line] = offset;
				return offset;
			} else {
				// console.log('compute from beginning!');
				return headHeight + line * lineHeight + this[DATA].lh.slice(0, line).reduce((p, v) => p + v, 0);
			}
		}
	}

	private computeScroll() {
		const anchor = this[DATA].anchor;
		const lineHeight = this.getLineHeight();
		const totalLines = this.getSortedData().length;
		const headHeight = this.headLine() ? this[DATA].head.height() : 0;
		const scrollHeight = totalLines * lineHeight + headHeight + this[DATA].lh.reduce((p, v) => p + v, 0);
		const scrollTop = this.scrollTop();
		let beginLine = Math.max(Math.ceil((scrollTop - headHeight) / lineHeight), 0);
		let beginLineOffset = this.getLineOffset(headHeight, lineHeight, beginLine);
		while (beginLineOffset > scrollTop + headHeight) {
			beginLine--;
			beginLineOffset -= this.getLineHeight() + (this[DATA].lh[beginLine] ? this[DATA].lh[beginLine] : 0);
		}
		if (beginLine < 0) beginLine = 0;
		const clientHeight = this.innerHeight();
		const pageLines = Math.floor(clientHeight / lineHeight) + 2;
		anchor.css({
			top: scrollHeight - this[DATA].config.borderBottom + 'px',
		});
		return {
			scrollTop,
			beginLine,
			beginLineOffset,
			pageLines,
			lineHeight,
			scrollHeight,
		};
	}

	protected override renderBody() {
		if (!this[0].isConnected) return false;
		// console.log('render body:', this.scrollTop());
		const info = this.computeScroll();
		if (info.beginLine === this[DATA].first) return false;
		this[DATA].first = info.beginLine;
		const columns = this.getColumns();
		const lastCol = columns[columns.length - 1];
		const data = this.getSortedData();
		this[DATA].body.html('');
		this.query(':scope>div.empty').detach();
		if (data.length > 0) {
			this[DATA].body.css({
				display: '',
			});
			const lines = data.slice(info.beginLine, info.beginLine + info.pageLines).map((row, rowIdx) => {
				const rowId = info.beginLine + rowIdx;
				const line = dom
					.div(`r${rowId}`, 'line', rowId === this.active() ? 'active' : '', this.isRowDisable(row) ? 'disable' : '')
					.prop('lineNo', rowId);
				let sectionValue: unknown;
				// eslint-disable-next-line @typescript-eslint/no-explicit-any
				if (this[DATA].section && (sectionValue = (row as any)[this[DATA].section.key])) {
					line.append(this.renderSection(dom.div(), sectionValue, row, rowId, lastCol));
				} else {
					line.append(
						...columns.map((col, colIdx) =>
							this.renderCell(dom.div(), row, info.beginLine + rowIdx, col, colIdx).css(this[DATA].css[colIdx] ?? {})
						)
					);
				}
				return line;
			});
			let offset = info.beginLineOffset;
			this[DATA].body.append(...lines).css('top', offset + 'px');
			let totalExtra = 0;
			lines.forEach((line, idx) => {
				const id = info.beginLine + idx;
				this[DATA].lt[id] = offset;
				const lh = line.height();
				const extra = line.height() - info.lineHeight;
				if (extra > 0) {
					if (!this[DATA].lh[id]) {
						totalExtra += extra;
						this[DATA].lh[id] = extra;
					}
				}
				offset += lh;
			});
			if (totalExtra > 0) {
				this[DATA].anchor.css({
					top: info.scrollHeight + totalExtra - this[DATA].config.borderBottom + 'px',
				});
			}
			this[DATA].lt.length = info.beginLine + info.pageLines;
			if (this.autoHeight()) {
				this.css({
					height: this[DATA].anchor.top() + 1 + 'px',
				});
			}
		} else if (this.showEmpty()) {
			this[DATA].body.css({
				display: 'none',
			});
			this.append(dom.div('empty').append(empty()));
			if (this.autoHeight()) {
				this.css({
					height: 'auto',
				});
			}
		} else if (this.autoHeight()) {
			this.css({
				height: info.lineHeight + 'px',
			});
		}
	}

	appendRows(rows: RowType[]): this {
		this[DATA].data = this[DATA].data.concat(rows);
		this[DATA].sorted = null;
		this[DATA].first = null;
		this.renderBody();
		return this;
	}

	autoHeight(): boolean;
	autoHeight(value: boolean): this;
	autoHeight(value?: boolean): this | boolean {
		if (value === undefined) {
			return this.attr('auto-height') != null;
		}
		this.attr('auto-height', value);
		return this;
	}
}

export function grid(...className: string[]): Grid {
	return new Grid().addClass(...className);
}
